'''
		beetFs
		Copyright 2010 Martin Eve

		This file is part of beetFs.

		beetFs is free software: you can redistribute it and/or modify
		it under the terms of the GNU General Public License as published by
		the Free Software Foundation, either version 3 of the License, or
		(at your option) any later version.

		beetFs is distributed in the hope that it will be useful,
		but WITHOUT ANY WARRANTY; without even the implied warranty of
		MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
		GNU General Public License for more details.

		You should have received a copy of the GNU General Public License
		along with beetFs.	If not, see <http://www.gnu.org/licenses/>.

'''

from beets.plugins import BeetsPlugin
from beets.ui import Subcommand
import beets
import logging
from string import Template
import operator
import os
import tempfile
import re
import fuse
import errno
import stat
import datetime
import time
import calendar
import mutagen
from mutagen import flac, id3
from mutagen.flac import FLAC, Padding, MetadataBlock, VCFLACDict, CueSheet, SeekTable, FLACNoHeaderError
from mutagen.id3 import ID3
import io
from io import BytesIO

path_format = "$artist/$album ($year) [$format_upper]/$track - $artist - $title.$format"

beetFs_command = Subcommand('mount', help='Mount a beets filesystem')
log = logging.getLogger('beets')


# FUSE version at the time of writing. Be compatible with this version.
fuse.fuse_python_api = (0, 2)

""" This is duplicated from Library. Ideally should be exposed from there.
"""

metadata_rw_fields = [
		('title',			'text'),
		('artist',		 'text'),
		('album',			'text'),
		('genre',			'text'),
		('composer',	 'text'),
		('grouping',	 'text'),
		('year',			 'int'),
		('month',			'int'),
		('day',				'int'),
		('track',			'int'),
		('tracktotal', 'int'),
		('disc',			 'int'),
		('disctotal',	'int'),
		('lyrics',		 'text'),
		('comments',	 'text'),
		('bpm',				'int'),
		('comp',			 'bool'),
]
metadata_fields = [
		('length',	'real'),
		('bitrate', 'int'),
] + metadata_rw_fields

metadata_keys = map(operator.itemgetter(0), metadata_fields)


def template_mapping(lib, item):
		""" Builds a template substitution map. Taken from library.py.
		"""
		mapping = {}
		for key in metadata_keys:
				value = getattr(item, key)
				# sanitize the value for inclusion in a path:
				# replace / and leading . with _
				if isinstance(value, basestring):
						value.replace(os.sep, '_')
						value = re.sub(r'[\\/:]|^\.', '_', value)
				elif key in ('track', 'tracktotal', 'disc', 'disctotal'):
						# pad with zeros
						value = '%02i' % value
				else:
						value = str(value)
				mapping[key] = value
				
		format = os.path.splitext(item.path)[1][1:]
		mapping['format'] = re.sub(r'[\\/:]|^\.', '_', format)
		mapping['format_upper'] = re.sub(r'[\\/:]|^\.', '_', format).upper()
		
		# fix dud entries
		if mapping['artist'] == '':
			mapping['artist'] = 'Unknown Artist'

		if mapping['album'] == '':
			mapping['album'] = 'Unknown Album'

		if mapping['year'] == '0':
			mapping['year'] = 'Unknown Year'
			
		if mapping['title'] == '':
			mapping['title'] = 'Unknown Track'

		return mapping

def mount(lib, config, opts, args):
		# check we have a command line argument
		if not args:
			raise beets.ui.UserError('no mountpoint specified')
			
		# build the in-memory folder structure
		global structure_split 
		structure_split = path_format.split("/")
		global structure_depth 
		structure_depth = len(structure_split)
		root = {}
		templates = {}
		global library 
		library = lib
		
		# establish a blank dictionary at each level of depth
		# also create templates for each level
		for level in range(0, structure_depth):
			#log.debug("Creating template %s" % structure_split[level])
			templates[level] = Template(structure_split[level])
	
		global directory_structure 
		directory_structure = FSNode({}, {})
		
		# iterate over items in library
		for item in lib.items():
			# build the template map
			mapping = template_mapping(lib, item)

			# do the substitutions
			level_subbed = {}
			for level in range(0, structure_depth):
				level_subbed[level] = templates[level].substitute(mapping)

			# build a directory structure
			sub_elements = []
			for level in range(0, structure_depth - 1):
				# append the path up to the current depth, minus one, to elements
				# this means that sub_elements contains the current path, except for the folder to be added
				if level-1 in level_subbed:
					sub_elements.append(level_subbed[level-1])					
				# add this directory to the master structure
				directory_structure.adddir(sub_elements, level_subbed[level])		
		
			# add this item as a file
			
			# First, compensate for 2 directories depth taken off above
			# ie. sub_elements now contains the full folder path
			sub_elements.append(level_subbed[structure_depth-3])	
			sub_elements.append(level_subbed[structure_depth-2])

			# do the actual add
			directory_structure.addfile(sub_elements, level_subbed[structure_depth-1], item.id)
			
		server = beetFileSystem(version="%prog " + fuse.__version__,
				usage="", dash_s_do='setsingle')
		server.parse(errex=1)
		
		server.multithreaded = 0
		try:
				server.main()
				pass
		except fuse.FuseError, e:
				log.error(str(e))
		

beetFs_command.func = mount

class beetFs(BeetsPlugin):
		""" The beets plugin hook
		"""
		def commands(self):
				return [beetFs_command]
				
import mutagen
from mutagen import flac
from mutagen.flac import FLAC, Padding, MetadataBlock

def to_int_be(string):
	"""Convert an arbitrarily-long string to a long using big-endian
	byte order."""
	return reduce(lambda a, b: (a << 8) + ord(b), string, 0L)

class InterpolatedID3 (ID3):
		def save(self, filename=None, v1=0):
				"""Save changes to a file.

				If no filename is given, the one most recently loaded is used.

				Keyword arguments:
				v1 -- if 0, ID3v1 tags will be removed
							if 1, ID3v1 tags will be updated but not added
							if 2, ID3v1 tags will be created and/or updated

				The lack of a way to update only an ID3v1 tag is intentional.
				"""

				# Sort frames by 'importance'
				order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"]
				order = dict(zip(order, range(len(order))))
				last = len(order)
				frames = self.items()
				frames.sort(lambda a, b: cmp(order.get(a[0][:4], last),
																		 order.get(b[0][:4], last)))

				framedata = [self.__save_frame(frame) for (key, frame) in frames]
				framedata.extend([data for data in self.unknown_frames
								if len(data) > 10])
				

				framedata = ''.join(framedata)
				framesize = len(framedata)

				if filename is None: filename = self.filename
				f = open(filename, 'rb+')
				try:
						idata = f.read(10)
						try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
						except struct.error: id3, insize = '', 0
						insize = BitPaddedInt(insize)
						if id3 != 'ID3': insize = -10

						if insize >= framesize: outsize = insize
						else: outsize = (framesize + 1023) & ~0x3FF
						framedata += '\x00' * (outsize - framesize)

						framesize = BitPaddedInt.to_str(outsize, width=4)
						flags = 0
						header = pack('>3sBBB4s', 'ID3', 4, 0, flags, framesize)
						data = header + framedata

						if (insize < outsize):
								insert_bytes(f, outsize-insize, insize+10)
						f.seek(0)

						try:
								f.seek(-128, 2)
						except IOError, err:
								from errno import EINVAL
								if err.errno != EINVAL: raise
								f.seek(0, 2) # ensure read won't get "TAG"

						if f.read(3) == "TAG":
								f.seek(-128, 2)
								if v1 > 0: f.write(MakeID3v1(self))
								else: f.truncate()
						elif v1 == 2:
								f.seek(0, 2)
								f.write(MakeID3v1(self))

				finally:
						f.close()

class InterpolatedFLAC (FLAC):

			def load(self, filedata):
				self.metadata_blocks = []
				self.tags = None
				self.cuesheet = None
				self.seektable = None
				#self.filename = filename
				self.filedata = filedata
				self.fileobj = BytesIO(filedata)
				self.__check_header(self.fileobj)

				while self.__read_metadata_block(self.fileobj):
						pass
				if self.fileobj.read(2) not in ["\xff\xf8", "\xff\xf9"]:
						raise FLACNoHeaderError("End of metadata did not start audio")

				try:
						self.metadata_blocks[0].length
				except (AttributeError, IndexError):
						raise FLACNoHeaderError("Stream info block not found")
						
				logging.info("Loaded INF")
				
			def __read_metadata_block(self, file):
				byte = ord(file.read(1))
				size = to_int_be(file.read(3))
				try:
						data = file.read(size)
						if len(data) != size:
								raise error(
										"file said %d bytes, read %d bytes" % (size, len(data)))
						block = self.METADATA_BLOCKS[byte & 0x7F](data)
				except (IndexError, TypeError):
						block = MetadataBlock(data)
						block.code = byte & 0x7F
						self.metadata_blocks.append(block)
				else:
						self.metadata_blocks.append(block)
						if block.code == VCFLACDict.code:
								if self.tags is None: self.tags = block
								else: raise FLACVorbisError("> 1 Vorbis comment block found")
						elif block.code == CueSheet.code:
								if self.cuesheet is None: self.cuesheet = block
								else: raise error("> 1 CueSheet block found")
						elif block.code == SeekTable.code:
								if self.seektable is None: self.seektable = block
								else: raise error("> 1 SeekTable block found")
				return (byte >> 7) ^ 1

			def get_header(self, filename=None):
				
				f = self.fileobj
				f.seek(0)

				# Ensure we've got padding at the end, and only at the end.
				# If adding makes it too large, we'll scale it down later.
				self.metadata_blocks.append(Padding('\x00' * 1020))
				MetadataBlock.group_padding(self.metadata_blocks)

				header = self.__check_header(f)
				available = self.__find_audio_offset(f) - header # "fLaC" and maybe ID3
				data = MetadataBlock.writeblocks(self.metadata_blocks)

				if len(data) > available:
						# If we have too much data, see if we can reduce padding.
						padding = self.metadata_blocks[-1]
						newlength = padding.length - (len(data) - available)
						if newlength > 0:
								padding.length = newlength
								data = MetadataBlock.writeblocks(self.metadata_blocks)
								assert len(data) == available
								
				elif len(data) < available:
						# If we have too little data, increase padding.
						self.metadata_blocks[-1].length += (available - len(data))
						data = MetadataBlock.writeblocks(self.metadata_blocks)
						assert len(data) == available

				if len(data) != available:
						# We couldn't reduce the padding enough.
						diff = (len(data) - available)
						
				self.__offset = len("fLaC" + data)

				return("fLaC" + data)	 
				
			def offset(self):
				return self.__offset
				
			def __find_audio_offset(self, fileobj):
				byte = 0x00
				while not (byte >> 7) & 1:
						byte = ord(fileobj.read(1))
						size = to_int_be(fileobj.read(3))
						fileobj.read(size)
				return fileobj.tell()

			def __check_header(self, fileobj):
				size = 4
				header = fileobj.read(4)
				if header != "fLaC":
						size = None
						if header[:3] == "ID3":
								size = 14 + BitPaddedInt(fileobj.read(6)[2:])
								fileobj.seek(size - 4)
								if fileobj.read(4) != "fLaC": size = None
				if size is None:
						raise FLACNoHeaderError(
								"%r is not a valid FLAC file" % fileobj.name)
				return size
				
class FSNode(object):
		"""A directory node. Contains directories (as a dictionary keyed
		by directory name) and files (dictionary keyed by filename to id).
		"""
		def __init__(self, dirs, files):
				self.dirs = dirs
				self.files = files

		def getnode(self, elements, root=None):
				if root == None:
					root = self
					
				if elements:
						topdir = elements.pop(0)
						return self.getnode(elements, root=root.dirs[topdir])
				else:
						# base case
						return root

		def adddir(self, elements, directory, root=None):
				if root == None:
					root = self
					
				if len(elements) == 1 and elements[0] == '':
						elements = []
				node = self.getnode(elements, root=root)
				if not directory in node.dirs:
					node.dirs[directory] = FSNode({}, {})
				
		def addfile(self, elements, filename, id, root=None):
				if root == None:
					root = self
					
				if len(elements) == 1 and elements[0] == '':
						elements = []
				node = self.getnode(elements, root=root)
				node.files[filename] = id

		def listdir(self, elements, directories, root=None):
				if root == None:
					root = self
					
				if len(elements) == 1 and elements[0] == '':
						elements = []
				node = self.getnode(elements, root=root)
				if directories:
					return node.dirs.keys()
				else:
					return node.files.keys()
					
class FileHandler(object):
	def __init__(self, path, lib):
		self.path = path
		self.lib = lib
		
		pathsplit = path[1:].split('/')		
		subdepth = path.count('/') + 1
			
		# determine the item and real path
		self.item = self.lib.get_item(id=directory_structure.getnode(pathsplit[0:structure_depth-1]).files[pathsplit[structure_depth-1]])
		self.real_path = self.item.path
		
		# open the on-disk file for reading
		self.file_object = open(self.real_path, 'r+')
		self.instance_count = 1
		
		# now get the bounds of the file_class
		#TODO: this needs to handle other file formats; use mutagen's detection procedure
		self.format = os.path.splitext(path)[1][1:].lower()
		#logging.info(self.real_path)
		if self.format == "flac":
			#self.inf = InterpolatedFLAC(self.real_path)
			self.inf = InterpolatedFLAC(self.file_object.read())
			
			# get values from database
			self.inf["title"] = self.item.title
			self.inf["album"] = self.item.album
			self.inf["artist"] = self.item.artist
			self.inf["genre"] = self.item.genre
			
			self.header = self.inf.get_header(self.real_path)
			self.bound = len(self.header)
			self.music_offset = self.inf.offset()
			
		elif self.format == "mp3":
			self.bound = 0 # disable interpolation for now
			self.music_offset = 0 # disable interpolation for now
		
		# read in the contents of the file
		self.file_object.seek(self.music_offset)
		self.music_data = self.file_object.read()
		# close the file
		self.file_object.close()
		
	def open(self):
		# as init() handles actual opening, just increment instance count here
		self.instance_count = self.instance_count + 1

	def release(self):
		# decrement the instance count
		if self.instance_count > 0:
			self.instance_count = self.instance_count - 1
			return False
		else:
			return True

	def read(self, size, offset):
		# check if read is within header boundary
		if offset < self.bound:
			
			if offset + size < len(self.header):
			# can just read from header
				logging.info("JUST HEADER")
				ret = self.header[offset:offset+size]
				return ret
			else:
				# get the header + some data from file
				logging.info("HEADER + DATA")
				ret = self.header[offset:len(self.header)]
				ret = ret + self.music_data[0:size - ((len(self.header) - offset))]
				return ret
		
		# otherwise, pass read call to underlying file system
		#self.file_object.seek(offset)
		logging.info("JUST MUSIC")
		return self.music_data[offset - len(self.header):offset - len(self.header) + size]
		
	def write(self, offset, buf):
		# determine if offset is within header; if not, discard write

		if offset < self.bound:
			# load interpolated file into memory

			filedata = self.header + self.music_data

			# patch in the new data
			filedata2 = filedata[0:offset] + buf + filedata[offset + len(buf):]
			filedata = filedata2
	
			# create a new normal mutagen object
			if self.format == "flac":
				try:
					# now obtain a new Interpolated FLAC from the temporary file
					self.inf = InterpolatedFLAC(filedata)

					# instead of putting the values into the FLAC, extract the values
					self.item.title = str(self.inf["title"][0]).encode('utf-8')					
					self.item.album = str(self.inf["album"][0]).encode('utf-8')
					self.item.artist = str(self.inf["artist"][0]).encode('utf-8')
					self.item.genre = str(self.inf["genre"][0]).encode('utf-8')

					self.lib.store(self.item)
					self.lib.save()
			
					# get values from database
					self.inf["title"] = self.item.title
					self.inf["album"] = self.item.album
					self.inf["artist"] = self.item.artist
					self.inf["genre"] = self.item.genre
			
					self.header = self.inf.get_header(self.real_path)
					self.bound = len(self.header)
					self.music_offset = self.inf.offset()
					
					return len(buf)
				except IOError:
					logging.error("Couldn't update tag.")
			pass
					
class Stat(fuse.Stat):
		DIRSIZE = 4096

		def __init__(self, st_mode, st_size, st_nlink=1, st_uid=None, st_gid=None,
						dt_atime=None, dt_mtime=None, dt_ctime=None):

				self.st_mode = st_mode
				self.st_ino = 0
				self.st_dev = 0
				self.st_nlink = st_nlink
				if st_uid is None:
						st_uid = os.getuid()
				self.st_uid = st_uid
				if st_gid is None:
						st_gid = os.getgid()
				self.st_gid = st_gid
				self.st_size = st_size
				now = datetime.datetime.utcnow()
				self.dt_atime = dt_atime or now
				self.dt_mtime = dt_mtime or now
				self.dt_ctime = dt_ctime or now

		def _get_dt_atime(self):
				return self.epoch_datetime(self.st_atime)
		def _set_dt_atime(self, value):
				self.st_atime = self.datetime_epoch(value)
		dt_atime = property(_get_dt_atime, _set_dt_atime)

		def _get_dt_mtime(self):
				return self.epoch_datetime(self.st_mtime)
		def _set_dt_mtime(self, value):
				self.st_mtime = self.datetime_epoch(value)
		dt_mtime = property(_get_dt_mtime, _set_dt_mtime)

		def _get_dt_ctime(self):
				return self.epoch_datetime(self.st_ctime)
		def _set_dt_ctime(self, value):
				self.st_ctime = self.datetime_epoch(value)
		dt_ctime = property(_get_dt_ctime, _set_dt_ctime)

		@staticmethod
		def datetime_epoch(dt):
				return calendar.timegm(dt.timetuple())
		@staticmethod
		def epoch_datetime(seconds):
				return datetime.datetime.utcfromtimestamp(seconds)
					
class beetFileSystem(fuse.Fuse):
		def __init__(self, *args, **kwargs):	
				LOG_FILENAME = "LOG"
				logging.basicConfig(filename=LOG_FILENAME,level=logging.INFO,)
				
				logging.info("Preparing to mount file system")
				super(beetFileSystem, self).__init__(*args, **kwargs)

		def fsinit(self):
				# called after filesystem is mounted
				#self.lib = self.cmdline[1][0]
				self.lib = library
				self.files = {}

				logging.info("Filesystem mounted")
				
		def fsdestroy(self):
				logging.info("Unmounting file system")

		def statfs(self):
				logging.info("statfs")

				# have no way of knowing where the music is stored
				# (disparate locations), so using homedir to fill this in
				return os.statvfs(os.path.expanduser("~"))

				
		def getattr(self, path):
				logging.info("getattr: %s" % path)
				
				try:
					if path == "/":
							logging.info("Returning /")
							mode = stat.S_IFDIR | 0755
							st = Stat(st_mode=mode, st_size=Stat.DIRSIZE, st_nlink=2)
							return st
					else:
						# determine if it's a directory or a file list
						# Split path into components
						pathsplit = path[1:].split('/')

						if len(pathsplit) == structure_depth:
							# it's a file						
							item = self.lib.get_item(directory_structure.getnode(pathsplit[0:structure_depth-1]).files[pathsplit[structure_depth-1]]).path

							if not item:
								# file not found
								logging.error("Returning ENOENT")
								return -errno.ENOENT
							elif item == '':
								logging.error("Returning ENOENT")
								return -errno.ENOENT
							statinfo = os.stat(item)
							st = Stat(st_mode=statinfo.st_mode, st_size=statinfo.st_size, st_uid=statinfo.st_uid, st_gid=statinfo.st_gid, st_nlink=statinfo.st_nlink, dt_atime=datetime.datetime.fromtimestamp(statinfo.st_atime), dt_mtime=datetime.datetime.fromtimestamp(statinfo.st_mtime), dt_ctime=datetime.datetime.fromtimestamp(statinfo.st_ctime))
							return st
						else:
								logging.info("dir")
								# it's a directory
								if not pathsplit[len(pathsplit)-1] in directory_structure.getnode(pathsplit[0:len(pathsplit)-1]).dirs:
									# directory not found
									logging.error("Returning ENOENT")
									return -errno.ENOENT
								else:
									logging.info("gotdir")
									mode = stat.S_IFDIR | 0544
									st = Stat(st_mode=mode, st_size=Stat.DIRSIZE, st_nlink=2)
									return st
									
				except Exception as e:
					logging.error(e)			
					return -errno.ENOENT
										
		# Note: utime is deprecated in favour of utimens.
		# utimens takes precedence over utime, so having this here does nothing
		# unless you delete utimens.
		def utime(self, path, times):

				atime, mtime = times
				logging.info("utime: %s (atime %s, mtime %s)" % (path, atime, mtime))
				return -errno.EOPNOTSUPP

		def utimens(self, path, atime, mtime):

				logging.info("utime: %s (atime %s:%s, mtime %s:%s)"
						% (path,atime.tv_sec,atime.tv_nsec,mtime.tv_sec,mtime.tv_nsec))
				return -errno.EOPNOTSUPP
				
		def access(self, path, flags):
				logging.info("access: %s (flags %s)" % (path, oct(flags)))
				pathsplit = path[1:].split('/')
				if path == "/":
					return 0
				else:
					is_dir = not len(pathsplit) == structure_depth

				# check for existence
				if is_dir:
					logging.info("dir")
					if not pathsplit[len(pathsplit) - 1] in directory_structure.getnode(pathsplit[0:len(pathsplit)-1]).dirs:
						return -errno.EACCES
					else:
						# if exists, always return allowed for directories
						return 0
				else:
					item = self.lib.get_item(id=directory_structure.getnode(pathsplit[0:structure_depth-1]).files[pathsplit[structure_depth-1]]).path
					if not item:
						return -errno.EACCES			
					else:
						# TODO: actually check the file permissions
						# NB. existence is already tested (os.F_OK)
						if flags | os.R_OK: 
							pass				
						if flags | os.W_OK: 
							pass
						if flags | os.X_OK: 
							pass
							
						return 0
						
		def readlink(self, path):
				"""
				Get the target of a symlink.
				Returns a bytestring with the contents of a symlink (its target).
				May also return an int error code.
				"""
				logging.info("readlink: %s" % path)
				return -errno.EOPNOTSUPP
				
		def mknod(self, path, mode, rdev):
				"""
				Creates a non-directory file (or a device node).
				mode: Unix file mode flags for the file being created.
				rdev: Special properties for creation of character or block special
						devices (I've never gotten this to work).
						Always 0 for regular files or FIFO buffers.
				"""
				# Note: mode & 0770000 gives you the non-permission bits.
				# Common ones:
				# S_IFREG:	0100000 (A regular file)
				# S_IFIFO:	010000	(A fifo buffer, created with mkfifo)

				# Potential ones (I have never seen them):
				# Note that these could be made by copying special devices or sockets
				# or using mknod, but I've never gotten FUSE to pass such a request
				# along.
				# S_IFCHR:	020000	(A character special device, created with mknod)
				# S_IFBLK:	060000	(A block special device, created with mknod)
				# S_IFSOCK: 0140000 (A socket, created with mkfifo)

				# Also note: You can use self.GetContext() to get a dictionary
				#	 {'uid': ?, 'gid': ?}, which tells you the uid/gid of the user
				#	 executing the current syscall. This should be handy when creating
				#	 new files and directories, because they should be owned by this
				#	 user/group.
				logging.info("mknod: %s (mode %s, rdev %s)" % (path, oct(mode), rdev))
				return -errno.EOPNOTSUPP

		def mkdir(self, path, mode):
				"""
				Creates a directory.
				mode: Unix file mode flags for the directory being created.
				"""
				# Note: mode & 0770000 gives you the non-permission bits.
				# Should be S_IDIR (040000); I guess you can assume this.
				# Also see note about self.GetContext() in mknod.
				logging.info("mkdir: %s (mode %s)" % (path, oct(mode)))
				return -errno.EOPNOTSUPP

		def unlink(self, path):
				"""Deletes a file."""
				logging.info("unlink: %s" % path)
				return -errno.EOPNOTSUPP

		def rmdir(self, path):
				"""Deletes a directory."""
				logging.info("rmdir: %s" % path)
				return -errno.EOPNOTSUPP

		def symlink(self, target, name):
				"""
				Creates a symbolic link from path to target.

				The 'name' is a regular path like any other method (absolute, but
				relative to the filesystem root).
				The 'target' is special - it works just like any symlink target. It
				may be absolute, in which case it is absolute on the user's system,
				NOT the mounted filesystem, or it may be relative. It should be
				treated as an opaque string - the filesystem implementation should not
				ever need to follow it (that is handled by the OS).

				Hence, if the operating system creates a link FROM this system TO
				another system, it will call this method with a target pointing
				outside the filesystem.
				If the operating system creates a link FROM some other system TO this
				system, it will not touch this system at all (symlinks do not depend
				on the target system unless followed).
				"""
				logging.info("symlink: target %s, name: %s" % (target, name))
				return -errno.EOPNOTSUPP

		def link(self, target, name):
				"""
				Creates a hard link from name to target. Note that both paths are
				relative to the mounted file system. Hard-links across systems are not
				supported.
				"""
				logging.info("link: target %s, name: %s" % (target, name))
				return -errno.EOPNOTSUPP

		def rename(self, old, new):
				"""
				Moves a file from old to new. (old and new are both full paths, and
				may not be in the same directory).
				
				Note that both paths are relative to the mounted file system.
				If the operating system needs to move files across systems, it will
				manually copy and delete the file, and this method will not be called.
				"""
				logging.info("rename: target %s, name: %s" % (old, new))
				return -errno.EOPNOTSUPP

		def chmod(self, path, mode):
				"""Changes the mode of a file or directory."""
				logging.info("chmod: %s (mode %s)" % (path, oct(mode)))
				return -errno.EOPNOTSUPP

		def chown(self, path, uid, gid):
				"""Changes the owner of a file or directory."""
				logging.info("chown: %s (uid %s, gid %s)" % (path, uid, gid))
				return -errno.EOPNOTSUPP

		def truncate(self, path, size):
				"""
				Shrink or expand a file to a given size.
				If 'size' is smaller than the existing file size, truncate it from the
				end.
				If 'size' if larger than the existing file size, extend it with null
				bytes.
				"""
				logging.info("truncate: %s (size %s)" % (path, size))
				return -errno.EOPNOTSUPP

		### DIRECTORY OPERATION METHODS ###
		# Methods in this section are operations for opening directories and
		# working on open directories.
		# "opendir" is the method for opening directories. It *may* return an
		# arbitrary Python object (not None or int), which is used as a dir
		# handle by the methods for working on directories.
		# All the other methods (readdir, fsyncdir, releasedir) are methods for
		# working on directories. They should all be prepared to accept an
		# optional dir-handle argument, which is whatever object "opendir"
		# returned.
		
		def opendir(self, path):
				"""
				Checks permissions for listing a directory.
				This should check the 'r' (read) permission on the directory.

				On success, *may* return an arbitrary Python object, which will be
				used as the "fh" argument to all the directory operation methods on
				the directory. Or, may just return None on success.
				On failure, should return a negative errno code.
				Should return -errno.EACCES if disallowed.
				"""
				logging.info("opendir: %s" % path)
				pathsplit = path[1:].split('/')
				if path == "/":
						return directory_structure
				else:
						if not pathsplit[len(pathsplit) - 1] in directory_structure.getnode(pathsplit[0:len(pathsplit)-1]).dirs:
							return -errno.EACCES
						else:
							return directory_structure.getnode(pathsplit[0:len(pathsplit)-1]).dirs[pathsplit[len(pathsplit) - 1]]
							
		def releasedir(self, path, dh=None):
				"""
				Closes an open directory. Allows filesystem to clean up.
				"""
				logging.info("releasedir: %s (dh %s)" % (path, dh))

		def fsyncdir(self, path, datasync, dh=None):
				"""
				Synchronises an open directory.
				datasync: If True, only flush user data, not metadata.
				"""
				logging.info("fsyncdir: %s (datasync %s, dh %s)"
						% (path, datasync, dh))
						
		def readdir(self, path, offset, dh=None):
				"""
				Generator function. Produces a directory listing.
				Yields individual fuse.Direntry objects, one per file in the
				directory. Should always yield at least "." and "..".
				Should yield nothing if the file is not a directory or does not exist.
				(Does not need to raise an error).

				offset: I don't know what this does, but I think it allows the OS to
				request starting the listing partway through (which I clearly don't
				yet support). Seems to always be 0 anyway.
				"""
				logging.info("readdir: %s (offset %s, dh %s)" % (path, offset, dh))
				
				yield fuse.Direntry(".")
				yield fuse.Direntry("..")
				
				try:
					pathsplit = path[1:].split('/')
		
			
					if dh == None:
						if path == "/":
							logging.info("dh assigned as root")
							dh = directory_structure
						else:
							dh = directory_structure.getnode(pathsplit[0:len(pathsplit)-1]).dirs[pathsplit[len(pathsplit)]]
			

					if len(pathsplit) == structure_depth - 1:
						# files
						logging.info("Yielding files: %s" % path)
						for files in directory_structure.listdir(pathsplit, False):
							logging.info("Yielding file: %s" % files.encode('utf-8'))
							yield fuse.Direntry(files.encode('utf-8'))
					else:
						# directories
						for files in directory_structure.listdir(pathsplit, True):
							#logging.info("Yielding dir: %s" % files.encode('utf-8'))
							yield fuse.Direntry(files.encode('utf-8'))
							
				except Exception as e:
					logging.error(e)
				
		### FILE OPERATION METHODS ###
		# Methods in this section are operations for opening files and working on
		# open files.
		# "open" and "create" are methods for opening files. They *may* return an
		# arbitrary Python object (not None or int), which is used as a file
		# handle by the methods for working on files.
		# All the other methods (fgetattr, release, read, write, fsync, flush,
		# ftruncate and lock) are methods for working on files. They should all be
		# prepared to accept an optional file-handle argument, which is whatever
		# object "open" or "create" returned.

		def open(self, path, flags):
				"""
				Open a file for reading/writing, and check permissions.
				flags: As described in man 2 open (Linux Programmer's Manual).
						ORing of several access flags, including one of os.O_RDONLY,
						os.O_WRONLY or os.O_RDWR. All other flags are in os as well.

				On success, *may* return an arbitrary Python object, which will be
				used as the "fh" argument to all the file operation methods on the
				file. Or, may just return None on success.
				On failure, should return a negative errno code.
				Should return -errno.EACCES if disallowed.
				"""
				logging.info("open: %s (flags %s)" % (path, oct(flags)))
				
				file_path = None
				
				try:
					if self.files == None:
						self.files = {"x":"y"}
						
					if path in self.files:
						# get a file object
						logging.info("Retrieving an existing File Handler for: %s" % path)
						self.files[path].open()
					else:
						# create a file open
						logging.info("Creating a File Handler for: %s" % path)
						self.files[path] = FileHandler(path, self.lib)
			
					return self.files[path]
				except Exception as e:
					logging.info("Error creating a File Handler: %s" % e)
					return -errno.EACCES
				
				
		def create(self, path, mode, rdev):
				"""
				Creates a file and opens it for writing.
				Will be called in favour of mknod+open, but it's optional (OS will
				fall back on that sequence).
				mode: Unix file mode flags for the file being created.
				rdev: Special properties for creation of character or block special
						devices (I've never gotten this to work).
						Always 0 for regular files or FIFO buffers.
				See "open" for return value.
				"""
				logging.info("create: %s (mode %s, rdev %s)" % (path,oct(mode),rdev))
				return -errno.EOPNOTSUPP

		def fgetattr(self, path, fh=None):
				"""
				Retrieves information about a file (the "stat" of a file).
				Same as Fuse.getattr, but may be given a file handle to an open file,
				so it can use that instead of having to look up the path.
				"""
				logging.info("fgetattr: %s (fh %s)" % (path, fh))
				# We could use fh for a more efficient lookup. Here we just call the
				# non-file-handle version, getattr.
				return self.getattr(path)

		def release(self, path, flags, fh=None):
				"""
				Closes an open file. Allows filesystem to clean up.
				flags: The same flags the file was opened with (see open).
				"""
				logging.info("release: %s (flags %s, fh %s)" % (path, oct(flags), fh))
				if self.files[path].release():
					logging.info("Complete release: %s (flags %s, fh %s)" % (path, oct(flags), fh))
					del self.files[path]

		def fsync(self, path, datasync, fh=None):
				"""
				Synchronises an open file.
				datasync: If True, only flush user data, not metadata.
				"""
				logging.info("fsync: %s (datasync %s, fh %s)" % (path, datasync, fh))

		def flush(self, path, fh=None):
				"""
				Flush cached data to the file system.
				This is NOT an fsync (I think the difference is fsync goes both ways,
				while flush is just one-way).
				"""
				logging.info("flush: %s (fh %s)" % (path, fh))
				
		def read(self, path, size, offset, fh=None):
				"""
				Get all or part of the contents of a file.
				size: Size in bytes to read.
				offset: Offset in bytes from the start of the file to read from.
				Does not need to check access rights (operating system will always
				call access or open first).
				Returns a byte string with the contents of the file, with a length no
				greater than 'size'. May also return an int error code.

				If the length of the returned string is 0, it indicates the end of the
				file, and the OS will not request any more. If the length is nonzero,
				the OS may request more bytes later.
				To signal that it is NOT the end of file, but no bytes are presently
				available (and it is a non-blocking read), return -errno.EAGAIN.
				If it is a blocking read, just block until ready.
				"""
				logging.info("read: %s (size %s, offset %s, fh %s)"
						% (path, size, offset, fh))
						
				
				if fh == None:
					file_path = None
				
					try:
						if self.files == None:
							self.files = {"x":"y"}
						self.files[path] = FileHandler(path, self.lib)
					except:
						return -errno.EPERM
		
				return self.files[path].read(size, offset)
			 
		def write(self, path, buf, offset, fh=None):
				"""
				Write over part of a file.
				buf: Byte string containing the text to write.
				offset: Offset in bytes from the start of the file to write to.
				Does not need to check access rights (operating system will always
				call access or open first).
				Should only overwrite the part of the file from offset to
				offset+len(buf).

				Must return an int: the number of bytes successfully written (should
				be equal to len(buf) unless an error occured). May also be a negative
				int, which is an errno code.
				"""
				logging.info("write: %s (offset %s, fh %s)" % (path, offset, fh))

				if fh == None:
					file_path = None
				
					try:
						if self.files == None:
							self.files = {"x":"y"}
						self.files[path] = FileHandler(path, self.lib)
					except:
						return -errno.EPERM
		
				try:
					return self.files[path].write(offset, buf)
				except Exception as ex:
					logging.info(ex)

		def ftruncate(self, path, size, fh=None):
				"""
				Shrink or expand a file to a given size.
				Same as Fuse.truncate, but may be given a file handle to an open file,
				so it can use that instead of having to look up the path.
				"""
				logging.info("ftruncate: %s (size %s, fh %s)" % (path, size, fh))
				return -errno.EOPNOTSUPP


